#include "platform.h"

#include <stdlib.h>

#if defined(_solaris_)
#include <stdlib.h>
#elif defined(_darwin_)
#include <mach-o/dyld.h>
#elif defined(_win_)
#include "winint.h"
#include <io.h>
#elif defined(_linux_)
#include <unistd.h>
#elif defined(_freebsd_)
#include <string.h>
#include <sys/types.h> // for u_int not defined in sysctl.h
#include <sys/sysctl.h>
#include <unistd.h>
#endif

#include <util/folder/dirut.h>
#include <util/generic/singleton.h>
#include <util/generic/yexception.h>
#include <util/memory/tempbuf.h>
#include <util/stream/file.h>
#include <util/stream/pipe.h>
#include <util/string/cast.h>
#include "filemap.h"

#include "execpath.h"
#include "fs.h"

#if defined(_freebsd_)
static inline bool GoodPath(const TString& path) {
    return path.find('/') != TString::npos;
}

static inline int FreeBSDSysCtl(int* mib, size_t mibSize, TTempBuf& res) {
    for (size_t i = 0; i < 2; ++i) {
        size_t cb = res.Size();
        if (sysctl(mib, mibSize, res.Data(), &cb, nullptr, 0) == 0) {
            res.Proceed(cb);
            return 0;
        } else if (errno == ENOMEM) {
            res = TTempBuf(cb);
        } else {
            return errno;
        }
    }
    return errno;
}

static inline TString FreeBSDGetExecPath() {
    int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
    TTempBuf buf;
    int r = FreeBSDSysCtl(mib, Y_ARRAY_SIZE(mib), buf);
    if (r == 0) {
        return TString(buf.Data(), buf.Filled() - 1);
    } else if (r == ENOTSUP) { // older FreeBSD version
        TString path("/proc/" + ToString(getpid()) + "/file");
        return NFs::ReadLink(path);
    } else {
        return TString();
    }
}

static inline TString FreeBSDGetArgv0() {
    int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_ARGS, getpid()};
    TTempBuf buf;
    int r = FreeBSDSysCtl(mib, Y_ARRAY_SIZE(mib), buf);
    if (r == 0) {
        return TString(buf.Data());
    } else if (r == ENOTSUP) {
        return TString();
    } else {
        ythrow yexception() << "FreeBSDGetArgv0() failed: " << LastSystemErrorText();
    }
}

static inline bool FreeBSDGuessExecPath(const TString& guessPath, TString& execPath) {
    if (NFs::Exists(guessPath)) {
        // now it should work for real
        execPath = FreeBSDGetExecPath();
        if (RealPath(execPath) == RealPath(guessPath)) {
            return true;
        }
    }
    return false;
}

static inline bool FreeBSDGuessExecBasePath(const TString& guessBasePath, TString& execPath) {
    return FreeBSDGuessExecPath(TString(guessBasePath) + "/" + getprogname(), execPath);
}

#endif

static TString GetExecPathImpl() {
#if defined(_solaris_)
    return execname();
#elif defined(_darwin_)
    TTempBuf execNameBuf;
    for (size_t i = 0; i < 2; ++i) {
        uint32_t bufsize = (uint32_t)execNameBuf.Size();
        int r = _NSGetExecutablePath(execNameBuf.Data(), &bufsize);
        if (r == 0) {
            return execNameBuf.Data();
        } else if (r == -1) {
            execNameBuf = TTempBuf(bufsize);
        }
    }
    ythrow yexception() << "GetExecPathImpl() failed";
#elif defined(_win_)
    TTempBuf execNameBuf;
    for (;;) {
        DWORD r = GetModuleFileName(nullptr, execNameBuf.Data(), execNameBuf.Size());
        if (r == execNameBuf.Size()) {
            execNameBuf = TTempBuf(execNameBuf.Size() * 2);
        } else if (r == 0) {
            ythrow yexception() << "GetExecPathImpl() failed: " << LastSystemErrorText();
        } else {
            return execNameBuf.Data();
        }
    }
#elif defined(_linux_) || defined(_cygwin_)
    TString path("/proc/" + ToString(getpid()) + "/exe");
    return NFs::ReadLink(path);
// TODO(yoda): check if the filename ends with " (deleted)"
#elif defined(_freebsd_)
    TString execPath = FreeBSDGetExecPath();
    if (GoodPath(execPath)) {
        return execPath;
    }
    if (FreeBSDGuessExecPath(FreeBSDGetArgv0(), execPath)) {
        return execPath;
    }
    if (FreeBSDGuessExecPath(getenv("_"), execPath)) {
        return execPath;
    }
    if (FreeBSDGuessExecBasePath(getenv("PWD"), execPath)) {
        return execPath;
    }
    if (FreeBSDGuessExecBasePath(~NFs::CurrentWorkingDirectory(), execPath)) {
        return execPath;
    }

    ythrow yexception() << "can not resolve exec path";
#else
#error dont know how to implement GetExecPath on this platform
#endif
}

namespace {
    struct TExecPathHolder {
        inline TExecPathHolder() {
            ExecPath = GetExecPathImpl();
        }

        TString ExecPath;
    };
}

const TString& GetExecPath() {
    return SingletonWithPriority<TExecPathHolder, 1>()->ExecPath;
}

TMappedFile* OpenExecFile() {
    TString path = GetExecPathImpl();
    THolder<TMappedFile> mf(new TMappedFile(path));

    TString path2 = GetExecPathImpl();
    if (path != path2)
        ythrow yexception() << "OpenExecFile(): something happened to the binary while we were opening it: "
                               "filename changed 'on the fly' from <"
                            << path << "> to <" << path2 << ">";

    return mf.Release();
}
